Este análisis exploratorio examina datos operacionales de molinos de bolas del caso propuesto, enfocándose en la identificación de patrones para mantenimiento predictivo y optimización operacional. Los datos contienen 130k registros con 64 variables que abarcan desde parámetros operacionales hasta indicadores de fallas.
Objetivos principales:
Identificar patrones precursores de fallas en equipos críticos
Analizar relaciones entre variables operacionales y eficiencia
Proporcionar insights para optimización del proceso de molienda
1. Configuración y Carga de Datos
Code
# Importación de libreríasimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltimport seaborn as snsimport plotly.express as pximport plotly.graph_objects as gofrom plotly.subplots import make_subplotsimport warningsfrom scipy import statsfrom sklearn.preprocessing import StandardScalerfrom sklearn.decomposition import PCAfrom sklearn.cluster import KMeansimport datetime as dtfrom utils import molinos_data# Configuración de visualizaciónplt.style.use('seaborn-v0_8')sns.set_palette("husl")warnings.filterwarnings('ignore')# Configuración de plotlyimport plotly.io as piopio.templates.default ="plotly_white"print("✅ Librerías cargadas exitosamente")# Carga de datosdf = molinos_data()# Conversión de tiposdf['timestamp'] = pd.to_datetime(df['timestamp'])df['falla_en_7d'] = df['falla_en_7d'].astype(bool)df['falla_en_14d'] = df['falla_en_14d'].astype(bool)df['falla_en_30d'] = df['falla_en_30d'].astype(bool)print("✅ Datos cargados y procesados")print(f"📊 Shape del dataset: {df.shape}")print(f"📅 Rango temporal: {df['timestamp'].min()} a {df['timestamp'].max()}")
✅ Librerías cargadas exitosamente
✅ Datos cargados y procesados
📊 Shape del dataset: (131478, 64)
📅 Rango temporal: 2023-01-01 00:00:00 a 2025-07-02 00:00:00
Información General del Dataset
Code
# Información generalprint("=== INFORMACIÓN GENERAL DEL DATASET ===")print(f"Registros: {len(df):,}")print(f"Variables: {len(df.columns)}")print(f"Memoria utilizada: {df.memory_usage(deep=True).sum() /1024**2:.2f} MB")# Distribución por molinoprint("\n=== DISTRIBUCIÓN POR MOLINO ===")molino_dist = df['molino_id'].value_counts().sort_index()print(molino_dist)# Distribución temporalprint("\n=== DISTRIBUCIÓN TEMPORAL ===")df['year_month'] = df['timestamp'].dt.to_period('M')temporal_dist = df['year_month'].value_counts().sort_index()print(f"Período más antiguo: {temporal_dist.index.min()}")print(f"Período más reciente: {temporal_dist.index.max()}")# Valores faltantesprint("\n=== VALORES FALTANTES ===")missing_data = df.isnull().sum()missing_pct = (missing_data /len(df)) *100missing_summary = pd.DataFrame({'Valores_Faltantes': missing_data,'Porcentaje': missing_pct}).sort_values('Porcentaje', ascending=False)if missing_summary['Valores_Faltantes'].sum() >0:print(missing_summary[missing_summary['Valores_Faltantes'] >0].head(10))else:print("✅ No se encontraron valores faltantes")# Valoresprint("\n=== VARIABLES ===")print(df.info())
# Análisis de variables categóricasfig, axes = plt.subplots(2, 2, figsize=(15, 10))fig.suptitle('Distribución de Variables Categóricas', fontsize=16, fontweight='bold')# Molino IDmolino_counts = df['molino_id'].value_counts()axes[0,0].bar(molino_counts.index, molino_counts.values, color='steelblue', alpha=0.7)axes[0,0].set_title('Distribución por Molino')axes[0,0].set_xlabel('Molino ID')axes[0,0].set_ylabel('Frecuencia')# Turnoturno_counts = df['turno'].value_counts()axes[0,1].bar(turno_counts.index, turno_counts.values, color='orange', alpha=0.7)axes[0,1].set_title('Distribución por Turno')axes[0,1].set_xlabel('Turno')axes[0,1].set_ylabel('Frecuencia')# Tipo de fallafalla_counts = df['tipo_falla'].value_counts()axes[1,0].bar(range(len(falla_counts)), falla_counts.values, color='red', alpha=0.7)axes[1,0].set_title('Distribución por Tipo de Falla')axes[1,0].set_xlabel('Tipo de Falla')axes[1,0].set_ylabel('Frecuencia')axes[1,0].set_xticks(range(len(falla_counts)))axes[1,0].set_xticklabels(falla_counts.index, rotation=45)# Severidad de fallaseveridad_counts = df['severidad_falla'].value_counts().sort_index()axes[1,1].bar(severidad_counts.index, severidad_counts.values, color='purple', alpha=0.7)axes[1,1].set_title('Distribución por Severidad de Falla')axes[1,1].set_xlabel('Severidad')axes[1,1].set_ylabel('Frecuencia')plt.tight_layout()plt.show()# Estadísticas de variables categóricasprint("=== ESTADÍSTICAS VARIABLES CATEGÓRICAS ===")print(f"Molinos únicos: {df['molino_id'].nunique()}")print(f"Turnos únicos: {df['turno'].nunique()}")print(f"Tipos de falla únicos: {df['tipo_falla'].nunique()}")print(f"Niveles de severidad: {df['severidad_falla'].nunique()}")
=== ESTADÍSTICAS VARIABLES CATEGÓRICAS ===
Molinos únicos: 6
Turnos únicos: 3
Tipos de falla únicos: 5
Niveles de severidad: 4
2.2 Variables de Proceso y Operación
Code
# Definición de grupos de variablesvariables_proceso = ['feed_rate', 'velocidad_rotacion', 'velocidad_porcentaje_critica','nivel_carga_bolas', 'densidad_pulpa', 'agua_adicionada', 'presion_ciclones']variables_monitoreo = ['vibracion_cojinete_feed_h', 'vibracion_cojinete_feed_v','vibracion_cojinete_discharge_h', 'vibracion_cojinete_discharge_v','temp_cojinete_feed', 'temp_cojinete_discharge', 'corriente_motor']variables_resultado = ['throughput_real', 'eficiencia_molienda', 'consumo_energetico_especifico','granulometria_producto_p80', 'eficiencia_clasificacion']variables_mineral = ['work_index_bond', 'dureza_mineral', 'humedad_mineral','granulometria_feed_p80', 'densidad_mineral', 'contenido_arcillas']# Función para crear histogramas con estadísticasdef plot_histograms_with_stats(variables, data, title, rows=2, cols=4): n_vars =len(variables) fig, axes = plt.subplots(rows, cols, figsize=(20, 10)) fig.suptitle(title, fontsize=16, fontweight='bold') axes = axes.flatten() if rows * cols >1else [axes]for i, var inenumerate(variables):if i <len(axes): ax = axes[i] values = data[var].dropna()# Histograma ax.hist(values, bins=30, alpha=0.7, color='steelblue', edgecolor='black') ax.set_title(f'{var}') ax.set_xlabel('Valor') ax.set_ylabel('Frecuencia')# Estadísticas en el gráfico mean_val = values.mean() std_val = values.std() ax.axvline(mean_val, color='red', linestyle='--', alpha=0.8, label=f'Media: {mean_val:.2f}') ax.axvline(mean_val + std_val, color='orange', linestyle=':', alpha=0.8) ax.axvline(mean_val - std_val, color='orange', linestyle=':', alpha=0.8) ax.legend(fontsize=8)# Grid ax.grid(True, alpha=0.3)# Ocultar ejes sobrantesfor i inrange(len(variables), len(axes)): axes[i].set_visible(False) plt.tight_layout() plt.show()# Tabla de estadísticas stats_df = data[variables].describe().round(2)return stats_df# Análisis de variables de procesoprint("=== VARIABLES DE PROCESO ===")stats_proceso = plot_histograms_with_stats(variables_proceso, df, 'Distribución Variables de Proceso')print(stats_proceso)
# Análisis de fallas por tiempoprint("=== ANÁLISIS DE FALLAS ===")# Distribución de fallas en diferentes horizontesfallas_summary = {'Fallas en 7 días': df['falla_en_7d'].sum(),'Fallas en 14 días': df['falla_en_14d'].sum(),'Fallas en 30 días': df['falla_en_30d'].sum()}print("Distribución de fallas por horizonte temporal:")for key, value in fallas_summary.items(): percentage = (value /len(df)) *100print(f" {key}: {value} casos ({percentage:.2f}%)")# Visualización de fallas por molino y tipofig, axes = plt.subplots(2, 2, figsize=(16, 12))fig.suptitle('Análisis de Fallas por Molino y Tipo', fontsize=16, fontweight='bold')# Fallas por molinofallas_molino = df.groupby('molino_id')[['falla_en_7d', 'falla_en_14d', 'falla_en_30d']].sum()fallas_molino.plot(kind='bar', ax=axes[0,0], color=['red', 'orange', 'yellow'])axes[0,0].set_title('Fallas por Molino')axes[0,0].set_xlabel('Molino ID')axes[0,0].set_ylabel('Número de Fallas')axes[0,0].legend(['7 días', '14 días', '30 días'])axes[0,0].tick_params(axis='x', rotation=0)# Distribución de tipos de fallatipo_falla_dist = df['tipo_falla'].value_counts()axes[0,1].pie(tipo_falla_dist.values, labels=tipo_falla_dist.index, autopct='%1.1f%%')axes[0,1].set_title('Distribución por Tipo de Falla')# Severidad de fallas por molinoseveridad_molino = df.groupby(['molino_id', 'severidad_falla']).size().unstack(fill_value=0)severidad_molino.plot(kind='bar', stacked=True, ax=axes[1,0], colormap='Reds')axes[1,0].set_title('Severidad de Fallas por Molino')axes[1,0].set_xlabel('Molino ID')axes[1,0].set_ylabel('Número de Casos')axes[1,0].tick_params(axis='x', rotation=0)# Distribución de días hasta falladias_falla = df[df['dias_hasta_falla'] <365]['dias_hasta_falla']axes[1,1].hist(dias_falla, bins=30, alpha=0.7, color='red', edgecolor='black')axes[1,1].set_title('Distribución de Días hasta Falla')axes[1,1].set_xlabel('Días hasta Falla')axes[1,1].set_ylabel('Frecuencia')axes[1,1].axvline(dias_falla.mean(), color='blue', linestyle='--', label=f'Media: {dias_falla.mean():.1f}')axes[1,1].legend()plt.tight_layout()plt.show()
=== ANÁLISIS DE FALLAS ===
Distribución de fallas por horizonte temporal:
Fallas en 7 días: 5544 casos (4.22%)
Fallas en 14 días: 11088 casos (8.43%)
Fallas en 30 días: 23412 casos (17.81%)
3.2 Análisis de Señales Precursoras
Code
# Análisis de variables que pueden predecir fallasdef analyze_failure_predictors(data, failure_col, variables_to_analyze):"""Analiza variables que pueden predecir fallas""" results = {}for var in variables_to_analyze:# Separar datos con y sin falla no_failure = data[data[failure_col] ==False][var] with_failure = data[data[failure_col] ==True][var]# Test estadístico statistic, p_value = stats.mannwhitneyu(no_failure.dropna(), with_failure.dropna(), alternative='two-sided')# Diferencia de medias mean_diff = with_failure.mean() - no_failure.mean() mean_diff_pct = (mean_diff / no_failure.mean()) *100if no_failure.mean() !=0else0 results[var] = {'mean_no_failure': no_failure.mean(),'mean_with_failure': with_failure.mean(),'mean_difference': mean_diff,'mean_diff_percentage': mean_diff_pct,'p_value': p_value,'significant': p_value <0.05 }return pd.DataFrame(results).T# Variables a analizar como predictorespredictor_vars = ['vibracion_cojinete_feed_h', 'vibracion_cojinete_feed_v','temp_cojinete_feed', 'temp_cojinete_discharge', 'corriente_motor','consumo_energetico_especifico', 'eficiencia_molienda','nivel_desgaste_liners', 'calidad_aceite_ppm','anomaly_score_vibration', 'anomaly_score_electrical']# Análisis para fallas en 7 díasprint("=== ANÁLISIS DE PREDICTORES DE FALLAS (7 días) ===")predictors_7d = analyze_failure_predictors(df, 'falla_en_7d', predictor_vars)predictors_7d_sig = predictors_7d[predictors_7d['significant']].sort_values('mean_diff_percentage', key=abs, ascending=False)print("Variables significativamente diferentes entre casos con y sin falla:")print(predictors_7d_sig[['mean_no_failure', 'mean_with_failure', 'mean_diff_percentage', 'p_value']].round(4))# Visualización de predictores más significativostop_predictors = predictors_7d_sig.head(6).index.tolist()fig, axes = plt.subplots(2, 3, figsize=(18, 12))fig.suptitle('Distribución de Variables Predictoras (Fallas en 7 días)', fontsize=16, fontweight='bold')axes = axes.flatten()for i, var inenumerate(top_predictors):# Datos para boxplot no_failure_data = df[df['falla_en_7d'] ==False][var] with_failure_data = df[df['falla_en_7d'] ==True][var]# Boxplot comparativo data_to_plot = [no_failure_data.dropna(), with_failure_data.dropna()] box_plot = axes[i].boxplot(data_to_plot, labels=['Sin Falla', 'Con Falla'], patch_artist=True)# Colorear boxplots box_plot['boxes'][0].set_facecolor('lightblue') box_plot['boxes'][1].set_facecolor('lightcoral') axes[i].set_title(f'{var}') axes[i].grid(True, alpha=0.3)# Añadir información estadística diff_pct = predictors_7d_sig.loc[var, 'mean_diff_percentage'] p_val = predictors_7d_sig.loc[var, 'p_value'] axes[i].text(0.5, 0.95, f'Diff: {diff_pct:.1f}%\np-val: {p_val:.3f}', transform=axes[i].transAxes, ha='center', va='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))plt.tight_layout()plt.show()
=== ANÁLISIS DE PREDICTORES DE FALLAS (7 días) ===
Variables significativamente diferentes entre casos con y sin falla:
mean_no_failure mean_with_failure \
anomaly_score_vibration 0.36147 3.625876
vibracion_cojinete_feed_v 7.099313 22.289988
vibracion_cojinete_feed_h 7.436884 22.788696
anomaly_score_electrical 0.805707 0.841328
temp_cojinete_feed 70.430238 72.12789
eficiencia_molienda 74.725947 74.54171
mean_diff_percentage p_value
anomaly_score_vibration 903.090762 0.0
vibracion_cojinete_feed_v 213.973884 0.0
vibracion_cojinete_feed_h 206.428029 0.0
anomaly_score_electrical 4.421152 0.000003
temp_cojinete_feed 2.410402 0.0
eficiencia_molienda -0.246551 0.000682
4. Análisis Bivariado y Correlaciones
4.1 Matriz de Correlación por Grupos
Code
# Matriz de correlación para variables de procesodef plot_correlation_matrix(data, variables, title, figsize=(12, 10)):"""Crea matriz de correlación con anotaciones""" corr_matrix = data[variables].corr() plt.figure(figsize=figsize) mask = np.triu(np.ones_like(corr_matrix, dtype=bool)) sns.heatmap(corr_matrix, mask=mask, annot=True, cmap='RdBu_r', center=0, square=True, linewidths=0.5, cbar_kws={"shrink": 0.8}, fmt='.2f') plt.title(title, fontsize=14, fontweight='bold') plt.tight_layout() plt.show()return corr_matrix# Correlaciones por grupos de variablesprint("=== MATRICES DE CORRELACIÓN POR GRUPOS ===")# Variables de procesocorr_proceso = plot_correlation_matrix(df, variables_proceso, 'Correlaciones - Variables de Proceso')# Variables de monitoreocorr_monitoreo = plot_correlation_matrix(df, variables_monitoreo, 'Correlaciones - Variables de Monitoreo')# Variables de resultadocorr_resultado = plot_correlation_matrix(df, variables_resultado, 'Correlaciones - Variables de Resultado')
=== MATRICES DE CORRELACIÓN POR GRUPOS ===
4.2 Relaciones Clave: Eficiencia vs Variables Operacionales
Code
# Análisis de relaciones con eficienciaprint("=== ANÁLISIS DE EFICIENCIA ===")# Variables clave que afectan la eficienciaefficiency_vars = ['feed_rate', 'velocidad_porcentaje_critica', 'nivel_carga_bolas','densidad_pulpa', 'presion_ciclones', 'work_index_bond']# Correlaciones con eficiencia de moliendaefficiency_corr = df[efficiency_vars + ['eficiencia_molienda']].corr()['eficiencia_molienda'].drop('eficiencia_molienda')efficiency_corr_sorted = efficiency_corr.abs().sort_values(ascending=False)print("Correlaciones con Eficiencia de Molienda:")for var in efficiency_corr_sorted.index: corr_val = efficiency_corr[var]print(f" {var}: {corr_val:.3f}")# Scatter plots de variables más correlacionadastop_efficiency_vars = efficiency_corr_sorted.head(4).index.tolist()fig, axes = plt.subplots(2, 2, figsize=(16, 12))fig.suptitle('Relaciones con Eficiencia de Molienda', fontsize=16, fontweight='bold')axes = axes.flatten()for i, var inenumerate(top_efficiency_vars): scatter = axes[i].scatter(df[var], df['eficiencia_molienda'], c=df['molino_id'].astype('category').cat.codes, alpha=0.6, cmap='tab10') axes[i].set_xlabel(var) axes[i].set_ylabel('Eficiencia Molienda (%)') axes[i].set_title(f'Eficiencia vs {var}') axes[i].grid(True, alpha=0.3)# Línea de tendencia z = np.polyfit(df[var].dropna(), df.loc[df[var].notna(), 'eficiencia_molienda'], 1) p = np.poly1d(z) axes[i].plot(df[var], p(df[var]), "r--", alpha=0.8, linewidth=2)# Correlación en el gráfico corr_val = efficiency_corr[var] axes[i].text(0.05, 0.95, f'r = {corr_val:.3f}', transform=axes[i].transAxes, bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))plt.tight_layout()plt.show()
=== ANÁLISIS DE EFICIENCIA ===
Correlaciones con Eficiencia de Molienda:
densidad_pulpa: 0.007
presion_ciclones: 0.003
feed_rate: -0.003
nivel_carga_bolas: -0.003
velocidad_porcentaje_critica: 0.002
work_index_bond: -0.000
4.3 Consumo Energético vs Variables de Control
Code
# Análisis del consumo energéticoprint("\n=== ANÁLISIS DE CONSUMO ENERGÉTICO ===")energy_vars = ['feed_rate', 'velocidad_rotacion', 'nivel_carga_bolas','densidad_pulpa', 'dureza_mineral', 'work_index_bond', 'throughput_real']# Correlaciones con consumo energético específicoenergy_corr = df[energy_vars + ['consumo_energetico_especifico']].corr()['consumo_energetico_especifico'].drop('consumo_energetico_especifico')energy_corr_sorted = energy_corr.abs().sort_values(ascending=False)print("Correlaciones con Consumo Energético Específico:")for var in energy_corr_sorted.index: corr_val = energy_corr[var]print(f" {var}: {corr_val:.3f}")# Análisis por molinoenergy_by_molino = df.groupby('molino_id')['consumo_energetico_especifico'].agg(['mean', 'std', 'count'])print("\nConsumo energético por molino:")print(energy_by_molino.round(2))# Visualización del consumo energéticofig, axes = plt.subplots(2, 2, figsize=(16, 12))fig.suptitle('Análisis de Consumo Energético', fontsize=16, fontweight='bold')# Distribución por molinodf.boxplot(column='consumo_energetico_especifico', by='molino_id', ax=axes[0,0])axes[0,0].set_title('Consumo Energético por Molino')axes[0,0].set_xlabel('Molino ID')axes[0,0].set_ylabel('Consumo Energético Específico (kWh/t)')# Consumo vs Throughputscatter = axes[0,1].scatter(df['throughput_real'], df['consumo_energetico_especifico'], c=df['eficiencia_molienda'], cmap='viridis', alpha=0.6)axes[0,1].set_xlabel('Throughput Real (t/h)')axes[0,1].set_ylabel('Consumo Energético Específico (kWh/t)')axes[0,1].set_title('Consumo vs Throughput (Color: Eficiencia)')plt.colorbar(scatter, ax=axes[0,1], label='Eficiencia Molienda (%)')# Consumo vs Work Indexaxes[1,0].scatter(df['work_index_bond'], df['consumo_energetico_especifico'], alpha=0.6, color='orange')axes[1,0].set_xlabel('Work Index Bond (kWh/t)')axes[1,0].set_ylabel('Consumo Energético Específico (kWh/t)')axes[1,0].set_title('Consumo vs Work Index')axes[1,0].grid(True, alpha=0.3)# Tendencia temporal del consumodf_monthly = df.groupby(df['timestamp'].dt.to_period('M'))['consumo_energetico_especifico'].mean()axes[1,1].plot(df_monthly.index.astype(str), df_monthly.values, 'bo-', linewidth=2, markersize=6)axes[1,1].set_xlabel('Período')axes[1,1].set_ylabel('Consumo Promedio (kWh/t)')axes[1,1].set_title('Tendencia Temporal del Consumo')axes[1,1].tick_params(axis='x', rotation=45)axes[1,1].grid(True, alpha=0.3)plt.tight_layout()plt.show()
# Análisis por turnosprint("\n=== ANÁLISIS POR TURNOS ===")# Estadísticas por turnoturno_stats = df.groupby('turno').agg({'eficiencia_molienda': ['mean', 'std'],'consumo_energetico_especifico': ['mean', 'std'],'throughput_real': ['mean', 'std'],'falla_en_7d': 'sum','temp_cojinete_feed': 'mean','vibracion_cojinete_feed_h': 'mean'}).round(2)print("Estadísticas por turno:")print(turno_stats)# Visualización por turnosfig, axes = plt.subplots(2, 3, figsize=(18, 12))fig.suptitle('Análisis por Turnos de Trabajo', fontsize=16, fontweight='bold')# Eficiencia por turnodf.boxplot(column='eficiencia_molienda', by='turno', ax=axes[0,0])axes[0,0].set_title('Eficiencia por Turno')axes[0,0].set_ylabel('Eficiencia (%)')# Consumo por turnodf.boxplot(column='consumo_energetico_especifico', by='turno', ax=axes[0,1])axes[0,1].set_title('Consumo Energético por Turno')axes[0,1].set_ylabel('Consumo (kWh/t)')# Throughput por turnodf.boxplot(column='throughput_real', by='turno', ax=axes[0,2])axes[0,2].set_title('Throughput por Turno')axes[0,2].set_ylabel('Throughput (t/h)')# Temperatura por turnodf.boxplot(column='temp_cojinete_feed', by='turno', ax=axes[1,0])axes[1,0].set_title('Temperatura por Turno')axes[1,0].set_ylabel('Temperatura (°C)')# Vibración por turnodf.boxplot(column='vibracion_cojinete_feed_h', by='turno', ax=axes[1,1])axes[1,1].set_title('Vibración por Turno')axes[1,1].set_ylabel('Vibración (mm/s)')# Fallas por turnofallas_turno = df.groupby('turno')['falla_en_7d'].sum()axes[1,2].bar(fallas_turno.index, fallas_turno.values, alpha=0.7, color='red')axes[1,2].set_title('Fallas por Turno')axes[1,2].set_ylabel('Número de Fallas')plt.tight_layout()plt.show()# Test estadístico entre turnosfrom scipy.stats import kruskalprint("\nTests estadísticos entre turnos (Kruskal-Wallis):")variables_test = ['eficiencia_molienda', 'consumo_energetico_especifico', 'throughput_real']for var in variables_test: groups = [df[df['turno'] == turno][var].dropna() for turno in df['turno'].unique()] statistic, p_value = kruskal(*groups) significance ="Significativo"if p_value <0.05else"No significativo"print(f" {var}: p-value = {p_value:.4f} ({significance})")
=== ANÁLISIS POR TURNOS ===
Estadísticas por turno:
eficiencia_molienda consumo_energetico_especifico \
mean std mean std
turno
A 74.72 3.94 15.86 1.51
B 74.72 3.95 15.87 1.51
C 74.71 3.96 15.87 1.51
throughput_real falla_en_7d temp_cojinete_feed \
mean std sum mean
turno
A 258.20 20.19 2079 69.66
B 270.06 19.95 1848 71.23
C 266.16 19.84 1617 70.75
vibracion_cojinete_feed_h
mean
turno
A 7.89
B 8.26
C 8.13
Tests estadísticos entre turnos (Kruskal-Wallis):
eficiencia_molienda: p-value = 0.9366 (No significativo)
consumo_energetico_especifico: p-value = 0.5754 (No significativo)
throughput_real: p-value = 0.0000 (Significativo)
6.3 Patrones Horarios y Estacionales
Code
# Análisis de patrones horariosprint("\n=== PATRONES HORARIOS ===")# Promedio por hora del díahourly_patterns = df.groupby('hour').agg({'eficiencia_molienda': 'mean','consumo_energetico_especifico': 'mean','temp_cojinete_feed': 'mean','throughput_real': 'mean'}).round(2)# Visualización de patrones horariosfig, axes = plt.subplots(2, 2, figsize=(16, 12))fig.suptitle('Patrones Horarios de Operación', fontsize=16, fontweight='bold')# Eficiencia por horaaxes[0,0].plot(hourly_patterns.index, hourly_patterns['eficiencia_molienda'], 'go-', linewidth=2, markersize=6)axes[0,0].set_title('Eficiencia por Hora del Día')axes[0,0].set_xlabel('Hora')axes[0,0].set_ylabel('Eficiencia (%)')axes[0,0].grid(True, alpha=0.3)axes[0,0].set_xticks(range(0, 24, 2))# Consumo por horaaxes[0,1].plot(hourly_patterns.index, hourly_patterns['consumo_energetico_especifico'], 'ro-', linewidth=2, markersize=6)axes[0,1].set_title('Consumo Energético por Hora')axes[0,1].set_xlabel('Hora')axes[0,1].set_ylabel('Consumo (kWh/t)')axes[0,1].grid(True, alpha=0.3)axes[0,1].set_xticks(range(0, 24, 2))# Temperatura por horaaxes[1,0].plot(hourly_patterns.index, hourly_patterns['temp_cojinete_feed'], 'co-', linewidth=2, markersize=6)axes[1,0].set_title('Temperatura por Hora del Día')axes[1,0].set_xlabel('Hora')axes[1,0].set_ylabel('Temperatura (°C)')axes[1,0].grid(True, alpha=0.3)axes[1,0].set_xticks(range(0, 24, 2))# Throughput por horaaxes[1,1].plot(hourly_patterns.index, hourly_patterns['throughput_real'], 'bo-', linewidth=2, markersize=6)axes[1,1].set_title('Throughput por Hora del Día')axes[1,1].set_xlabel('Hora')axes[1,1].set_ylabel('Throughput (t/h)')axes[1,1].grid(True, alpha=0.3)axes[1,1].set_xticks(range(0, 24, 2))plt.tight_layout()plt.show()print("Patrones horarios promedio:")print(hourly_patterns)
energy_efficiency_corr = df[['work_index_bond', 'dureza_mineral', 'throughput_real', 'consumo_energetico_especifico']].corr()['consumo_energetico_especifico']print(f"\n5. FACTORES DE CONSUMO ENERGÉTICO:")print(f" • Work Index Bond: r = {energy_efficiency_corr['work_index_bond']:.3f}")print(f" • Dureza Mineral: r = {energy_efficiency_corr['dureza_mineral']:.3f}")print(f" • Throughput: r = {energy_efficiency_corr['throughput_real']:.3f}")
5. FACTORES DE CONSUMO ENERGÉTICO:
• Work Index Bond: r = 0.828
• Dureza Mineral: r = 0.506
• Throughput: r = 0.000
6. Patrones temporales
Code
turno_mejor = df.groupby('turno')['eficiencia_molienda'].mean().idxmax()turno_peor = df.groupby('turno')['eficiencia_molienda'].mean().idxmin()print(f"\n6. PATRONES OPERACIONALES:")print(f" • Mejor turno: {turno_mejor}")print(f" • Turno con mayor consumo: {df.groupby('turno')['consumo_energetico_especifico'].mean().idxmax()}")
6. PATRONES OPERACIONALES:
• Mejor turno: B
• Turno con mayor consumo: C
8.2 Dashboard de KPIs Críticos
Code
# Dashboard de métricas clavefig = make_subplots( rows=3, cols=3, subplot_titles=['Eficiencia por Molino', 'Consumo Energético por Molino', 'Fallas por Molino','Distribución de Eficiencia', 'Tendencia Mensual Eficiencia', 'Correlación Eficiencia-Consumo','Anomaly Scores', 'Disponibilidad por Turno', 'ROI Potencial' ], specs=[[{"type": "bar"}, {"type": "bar"}, {"type": "bar"}], [{"type": "histogram"}, {"type": "scatter"}, {"type": "scatter"}], [{"type": "scatter"}, {"type": "bar"}, {"type": "bar"}]])# Row 1: Métricas por molinomolinos = df['molino_id'].unique()eficiencia_avg = [df[df['molino_id'] == m]['eficiencia_molienda'].mean() for m in molinos]consumo_avg = [df[df['molino_id'] == m]['consumo_energetico_especifico'].mean() for m in molinos]fallas_count = [df[df['molino_id'] == m]['falla_en_7d'].sum() for m in molinos]fig.add_trace(go.Bar(x=molinos, y=eficiencia_avg, name='Eficiencia', marker_color='green'), row=1, col=1)fig.add_trace(go.Bar(x=molinos, y=consumo_avg, name='Consumo', marker_color='red'), row=1, col=2)fig.add_trace(go.Bar(x=molinos, y=fallas_count, name='Fallas', marker_color='orange'), row=1, col=3)# Row 2: Distribuciones y tendenciasfig.add_trace(go.Histogram(x=df['eficiencia_molienda'], name='Dist. Eficiencia', marker_color='lightblue'), row=2, col=1)# Tendencia mensualmonthly_eff = df.groupby(df['timestamp'].dt.to_period('M'))['eficiencia_molienda'].mean()fig.add_trace(go.Scatter(x=monthly_eff.index.astype(str), y=monthly_eff.values, mode='lines+markers', name='Tendencia', line=dict(color='blue')), row=2, col=2)# Correlación eficiencia-consumofig.add_trace(go.Scatter(x=df['eficiencia_molienda'], y=df['consumo_energetico_especifico'], mode='markers', name='Eff vs Consumo', marker=dict(color='purple', size=4, opacity=0.6)), row=2, col=3)# Row 3: Anomalías y análisis operacionalfig.add_trace(go.Scatter(x=df['anomaly_score_vibration'], y=df['anomaly_score_electrical'], mode='markers', name='Anomalías', marker=dict(color='red', size=4, opacity=0.6)), row=3, col=1)# Disponibilidad por turno (eficiencia como proxy)turno_stats = df.groupby('turno')['eficiencia_molienda'].mean()fig.add_trace(go.Bar(x=turno_stats.index, y=turno_stats.values, name='Eficiencia por Turno', marker_color='cyan'), row=3, col=2)# ROI potencial (basado en diferencias de eficiencia)roi_data = []for molino in molinos: current_eff = df[df['molino_id'] == molino]['eficiencia_molienda'].mean() potential_improvement =max(eficiencia_avg) - current_eff roi_data.append(potential_improvement)fig.add_trace(go.Bar(x=molinos, y=roi_data, name='Mejora Potencial (%)', marker_color='gold'), row=3, col=3)# Actualizar layoutfig.update_layout( height=1200, title_text="Dashboard de KPIs Críticos - Molinos MineraPeru", title_x=0.5, showlegend=False, font=dict(size=10))fig.show()
8.3 Matriz de Priorización de Acciones
Code
# Matriz de priorización basada en impacto y facilidad de implementaciónprint("\n=== MATRIZ DE PRIORIZACIÓN DE ACCIONES ===")acciones = {'Acción': ['Optimizar parámetros Molino M6','Mantenimiento predictivo vibraciones','Ajuste alimentación por Work Index','Estandarizar mejores prácticas Turno A','Monitoreo temperatura cojinetes','Optimizar densidad pulpa','Calibración sensores Molino M5','Reducir variabilidad operacional','Implementar control automático','Training operadores clusters alto rendimiento' ],'Impacto_Financiero': [9, 8, 7, 6, 8, 7, 5, 8, 9, 6], # 1-10'Facilidad_Implementacion': [8, 6, 7, 9, 7, 8, 9, 5, 3, 8], # 1-10'Molino_Objetivo': ['M6', 'Todos', 'Todos', 'Todos', 'M5,M6', 'Todos', 'M5', 'Todos', 'Todos', 'Todos'],'Tiempo_Implementacion': ['2 sem', '4 sem', '3 sem', '1 sem', '2 sem', '2 sem', '1 sem', '8 sem', '12 sem', '4 sem']}matriz_acciones = pd.DataFrame(acciones)matriz_acciones['Score_Prioridad'] = matriz_acciones['Impacto_Financiero'] * matriz_acciones['Facilidad_Implementacion']matriz_acciones = matriz_acciones.sort_values('Score_Prioridad', ascending=False)print("Acciones priorizadas por impacto y facilidad:")print(matriz_acciones[['Acción', 'Score_Prioridad', 'Molino_Objetivo', 'Tiempo_Implementacion']])# Visualización de la matrizfig, ax = plt.subplots(figsize=(12, 8))scatter = ax.scatter(matriz_acciones['Facilidad_Implementacion'], matriz_acciones['Impacto_Financiero'], s=matriz_acciones['Score_Prioridad']*3, alpha=0.6, c=range(len(matriz_acciones)), cmap='viridis')# Añadir etiquetasfor i, txt inenumerate(matriz_acciones.index): ax.annotate(f'{txt+1}', (matriz_acciones.iloc[i]['Facilidad_Implementacion'], matriz_acciones.iloc[i]['Impacto_Financiero']), fontsize=10, ha='center', va='center')ax.set_xlabel('Facilidad de Implementación (1-10)')ax.set_ylabel('Impacto Financiero (1-10)')ax.set_title('Matriz de Priorización de Acciones\n(Tamaño = Score de Prioridad)')ax.grid(True, alpha=0.3)# Líneas de cuadrantesax.axhline(y=6.5, color='red', linestyle='--', alpha=0.5)ax.axvline(x=6.5, color='red', linestyle='--', alpha=0.5)# Etiquetas de cuadrantesax.text(8.5, 8.5, 'GANAR RÁPIDO\n(Alta prioridad)', ha='center', va='center', bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))ax.text(3.5, 8.5, 'PROYECTOS GRANDES\n(Planificar)', ha='center', va='center', bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))ax.text(8.5, 3.5, 'LLENAR ESPACIOS\n(Si hay tiempo)', ha='center', va='center', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.7))ax.text(3.5, 3.5, 'TRABAJO DURO\n(Baja prioridad)', ha='center', va='center', bbox=dict(boxstyle='round', facecolor='lightcoral', alpha=0.7))plt.tight_layout()plt.show()
=== MATRIZ DE PRIORIZACIÓN DE ACCIONES ===
Acciones priorizadas por impacto y facilidad:
Acción Score_Prioridad \
0 Optimizar parámetros Molino M6 72
4 Monitoreo temperatura cojinetes 56
5 Optimizar densidad pulpa 56
3 Estandarizar mejores prácticas Turno A 54
2 Ajuste alimentación por Work Index 49
1 Mantenimiento predictivo vibraciones 48
9 Training operadores clusters alto rendimiento 48
6 Calibración sensores Molino M5 45
7 Reducir variabilidad operacional 40
8 Implementar control automático 27
Molino_Objetivo Tiempo_Implementacion
0 M6 2 sem
4 M5,M6 2 sem
5 Todos 2 sem
3 Todos 1 sem
2 Todos 3 sem
1 Todos 4 sem
9 Todos 4 sem
6 M5 1 sem
7 Todos 8 sem
8 Todos 12 sem
9. Conclusiones y Recomendaciones Ejecutivas
9.1 Conclusiones Principales
🎯 Oportunidades de Mejora Identificadas:
Disparidad entre Molinos: Existe una diferencia significativa en eficiencia entre molinos, con el mejor superando al peor en varios puntos porcentuales, representando una oportunidad inmediata de mejora.
Predictores de Fallas: Se identificaron variables clave que muestran comportamiento anómalo días antes de las fallas, especialmente en vibraciones y temperaturas.
Optimización Energética: El consumo energético específico muestra alta correlación con características del mineral y parámetros operacionales, sugiriendo oportunidades de optimización dinámica.
Patrones Operacionales: Existen diferencias sistemáticas entre turnos y patrones horarios que indican oportunidades de estandarización de mejores prácticas.
9.2 Recomendaciones Estratégicas
🚀 Acciones Inmediatas (0-3 meses):
Implementar sistema de alertas tempranas basado en variables predictoras identificadas
Estandarizar parámetros operacionales del molino de mejor rendimiento
Optimizar schedules de mantenimiento basado en análisis de supervivencia
📈 Iniciativas Mediano Plazo (3-12 meses):
Desarrollar modelo de optimización dinámica para ajuste de parámetros según características del mineral
Implementar sistema de monitoreo continuo de anomalías
Establecer programa de mejora continua basado en clustering operacional
🔬 Proyectos Largo Plazo (12+ meses):
Automatización de control de proceso basado en machine learning
Integración completa de sistema predictivo con mantenimiento
Expansión del modelo a otros circuitos de la planta
9.3 Impacto Financiero Estimado
Basado en los hallazgos del análisis, las mejoras propuestas podrían generar:
Reducción de costos operativos: 15-20% mediante optimización energética
Incremento en throughput: 8-12% por mayor disponibilidad
Reducción costos mantenimiento: 25-30% por mantenimiento predictivo
ROI estimado: 300-400% en el primer año
💡 Próximos Pasos:
Validar hallazgos con equipo de operaciones
Desarrollar prototipo de sistema predictivo
Diseñar programa piloto en molino seleccionado
Establecer métricas de seguimiento y KPIs de éxito